from threading import Thread, Lock  # 线程库
from time import sleep  # 时间库
from tkinter import filedialog, Tk  # 打开文件库
from pyautogui import hotkey, press, center, locateOnScreen, click, alert
from pandas import read_excel  # 读取
from pynput import keyboard  # 键盘监控
from win32gui import FindWindow, GetWindowPlacement, ShowWindow, SetWindowPos
from win32con import SW_SHOWDEFAULT, HWND_TOPMOST, HWND_NOTOPMOST, SWP_SHOWWINDOW  # 控制窗体大小和鼠标移动
from pyperclip import copy  # 复制文字
from argparse import ArgumentParser  # 命令行输入
from datetime import datetime  # 时间，用来计算时间
from sys import setrecursionlimit  # 用来设置递归深度
from os import system  # 调用系统服务
from os.path import exists

setrecursionlimit(100000)  # 设置递归深度

__author__ = '广大菜鸟'
isEnd = False
listener = None
lock = Lock()  # 申请一把互斥锁


def on_activate():
    print('中断热键 <shift> 已启动.在此为本次程序异常对您造成的不便感到十分抱歉！！！')
    global isEnd
    isEnd = True
    return False


def for_canonical(f):
    global listener
    lock.acquire()  # 加锁
    pass
    lock.release()  # 释放锁
    return lambda k: f(listener.canonical(k))


HOTKEY = keyboard.HotKey(
    keyboard.HotKey.parse('<shift>'),
    on_activate)


def getWorkingLocation():
    window = FindWindow("WeWorkWindow", u"企业微信")  # 可以通过winSpy获得
    max_iter = 100
    while window == 0 and max_iter > 0:
        sleep(0.2)
        print("在定位窗口句柄时，发现为无效的窗口句柄。请检查是否打开企业微信的窗口后再重试此软件")
        window = FindWindow("WeWorkWindow", u"企业微信")  # 可以通过winSpy获得 (类名 ,标题)
        max_iter -= 1
    ShowWindow(window, SW_SHOWDEFAULT)  # 设置大小为正常模式,处理开启但没显示，最小化和最大化的情况
    return window, GetWindowPlacement(window)


def CTRL_F():
    hotkey('ctrl', 'f')


def CTRL_A():
    hotkey('ctrl', 'a')


def CTRL_V():
    hotkey('ctrl', 'v')


def ENTER():
    press('enter')


# 判定左侧在系统上的位置
def getFirstScreen():
    location = locateOnScreen(image='firstLocation.png')
    if location is None:
        location = locateOnScreen(image='firstLocation1.png')
        if location is None:
            return False
    # 输出坐标
    # 利用center()函数获取目标图像在系统中的中心坐标位置
    x, y = center(location)
    # 对识别出的目标图像进行点击
    # 参数x,y代表坐标位置，clicks代表点击次数,button可以设置为左键或者右键
    click(x=x, y=y, clicks=1, button='left')
    return True


# 获取用户名片区
def getTrueUserScreen(Region):
    # 判定目标截图在系统上的位置
    location = locateOnScreen(image='searchSNO.png', region=Region)  # 部分区域
    failsafe_count = 0
    while location is None and failsafe_count < 3:
        ENTER()
        location = locateOnScreen(image='searchSNO.png', region=Region)  # 部分区域
        failsafe_count += 1
    if location is None:
        return False
    x, y = center(location)
    click(x=x, y=y + 25, clicks=1, button='left')
    return True


# 获取输入框
def getChatScreen(thirdX, thirdY):
    # 判定目标截图在系统上的位置
    imgFile = r'sendInfo.png'
    x, y = thirdX, thirdY  # 默认比例
    if exists(imgFile):
        location = locateOnScreen(image=imgFile)
        if location is not None:
            # 输出坐标
            # 利用center()函数获取目标图像在系统中的中心坐标位置
            x, y = center(location)
            # 对识别出的目标图像进行点击
    # 参数x,y代表坐标位置，clicks代表点击次数,button可以设置为左键或者右键
    click(x=x, y=y, clicks=2, button='left')
    return True


# 输入学生的学号，前提：需要打开电脑的企业微信
def openUser(info, lang=None):
    # 0、 选择搜索页面
    window, loginid = getWorkingLocation()
    [left, top, _, _] = loginid[4]  # [left, top, right, bottom]
    if left < 0:  # 处理左侧被挡住
        left = 10

    width, height = 10, 10  # 用不可能的尺寸缩小到最小
    SetWindowPos(window, HWND_TOPMOST, left, top, width, height, SWP_SHOWWINDOW)  # 先设置为置顶,不可以改变位置
    SetWindowPos(window, HWND_NOTOPMOST, left, top, width, height,
                 SWP_SHOWWINDOW)  # 修改为可改变在前面的软件界面，考虑到在遇到意外需要手动关闭终端

    [left, top, right, bottom] = GetWindowPlacement(window)[4]  # 获取最小化后的参数
    width, height = right - left, bottom - top  # 最终的参数

    region = (left-10, top-10, int(width), int(height))
    thirdX, thirdY = int(right - 0.1 * width), int(bottom - height * 0.1)

    # 通过像素获取左侧聊天界面按钮，同时暂时覆盖软件页面
    firstBool = getFirstScreen()
    if not firstBool:
        print('请检查带有左侧问号框的截图')
        return

    global isEnd
    global listener
    isEnd = False
    listener = keyboard.Listener(
        on_press=for_canonical(HOTKEY.press))
    listener.daemon = 1

    # 创建、启动鼠标监听线程
    t2 = Thread(target=sendInfo,
                args=(info, region, thirdX, thirdY, lang))
    t2.daemon = 1
    listener.start()
    t2.start()
    while True:
        if isEnd:
            return


def sendInfo(info, region, thirdX, thirdY, lang):
    global isEnd
    for (sno, sname) in info:
        lock.acquire()  # 加锁
        if isEnd:
            return
        copy(sno)  # 复制到剪贴板
        # 搜索区
        CTRL_F()  # ctrl+F
        CTRL_A()
        CTRL_V()
        ENTER()
        # sleep(1)
        # 判断区
        userJudge = getTrueUserScreen(region)

        if not userJudge:
            print('没有通知到位的学生：', sno, " ", sname)
            lock.release()  # 释放锁
            continue

        # 发信息区
        msg = "你好，" + sname + "同学。"
        if lang is not None:
            msg = msg + lang
        else:
            msg += "温馨提醒，疫情防控，从我做起。截止于下午3点半，系统显示您今日还没有上报健康信息，请尽快上报。"

        copy(msg)
        getChatScreen(thirdX, thirdY)
        CTRL_V()
        ENTER()
        lock.release()  # 释放锁
        sleep(0.1)  # 给检查和处理异常的有效时间,线程可以切换检查是否有按下快捷键
    isEnd = True


def readFromExcelByPandas(filename):
    try:
        df = read_excel(filename, header=1, index_col=0, dtype=str)
        return df
    except Exception as e:
        print(str(e))
        return None


def informUsers(src, classno=None, lang=None, delGrade=None):
    itemDataFrame = readFromExcelByPandas(src)
    if itemDataFrame is None:
        return
    itemDataFrame.fillna(' ', inplace=True)

    if classno is not None and '班级' in itemDataFrame.columns:
        target = itemDataFrame.loc[itemDataFrame['班级'] == classno]  # 建立在表格的用户的sno都存在
    else:
        target = itemDataFrame

    # 默认删除17级
    if '班级' in target.columns:
        target = target.loc[~ target['班级'].str.contains('[^0-9]+' + str(17) + '\d')]  # 默认先删除17级
        if delGrade is not None and 10 < delGrade < 100:  # 目前可见年级有17-20，所以合理范围为2位数 去除
            target = target.loc[~ target['班级'].str.contains('[^0-9]+' + str(delGrade) + '\d')]  # 正则表达式匹配 如"计科214"的21

    print("本次需要选择的对象数目有：", len(target))
    # openUser(list(target.groupby(['学号', '姓名']).groups.keys()), lang) # 按照学号排序
    if len(target) > 0:
        openUser(target[['学号', '姓名']].values.tolist(), lang)  # 按原来的顺序


def informUser(sno, sname, lang=None):
    openUser([(sno, sname)], lang)


# 选择文件
def selectFilename():
    root = Tk()
    root.withdraw()
    filename = filedialog.askopenfilename()
    if filename != '':
        print("您选择的文件是：" + filename)
        return filename
    else:
        raise Exception("您没有选择任何文件")


# 结束通知
def EndInfo():
    alert(title='提示', text='本次通知任务结束')


def main():
    parser = ArgumentParser()
    parser.description = '本软件功能是企业微信打卡，可以选择文件/个人模式(注：确定企业微信最新版，在启动企业微信和该程序后，不要移动鼠标和点击键盘)\n' \
                         '\n默认发送消息格式为\'你好，xxx同学。\'\n' \
                         '简单使用案例：企业微信打卡提醒 -c *  -r 21 (意思是删除表中21届的学生，不给于通知)'
    parser.add_argument("-c", "--classno", help="这个参数是班级名称(当*时，为操作所有班级数据)", type=str)
    parser.add_argument("-o", "--sno", help="这个参数是学生学号", type=str)
    parser.add_argument("-a", "--sname", help="这个参数是学生名字", type=str)
    parser.add_argument("-l", "--lang", help="这个参数是在基本发送格式[你好，xxx同学。]增加的文字。如不设置，则输出自定的格式。", type=str)
    parser.add_argument("-r", "--delGrade", help="这个参数是删除年级（2位数字，如17）", type=int, default=17)
    # parser.add_argument("-t", "--time", help="这个参数是等待响应时间（可以根据网速增加,默认0.3(单位：秒)，只能多不能少，少了肯定有异常)", type=float)
    args = parser.parse_args()

    if args.sname is not None and args.sno is not None:
        oldtime = datetime.now()
        try:
            informUser(args.sno, args.sname, args.lang)
        except Exception as e:
            print(str(e))
        finally:
            newtime = datetime.now()
            print("本次通知使用时间为%.2fs" % ((newtime - oldtime).total_seconds()))
            EndInfo()
    elif args.classno is not None:  # 文件+班级模式
        classno = args.classno if args.classno != '*' else None
        oldtime = datetime.now()  # 包括选择文件的时间
        try:
            srcFilePath = selectFilename()
            informUsers(srcFilePath, classno, args.lang, args.delGrade)
        except Exception as e:
            print(str(e))
        finally:
            newtime = datetime.now()
            print("本次通知使用时间为%.2fs" % ((newtime - oldtime).total_seconds()))
            EndInfo()
    else:
        try:
            parser.error('\n请使用终端命令行启动本软件，并输入参数\n选择文件模式时，需要指定班级名称或者用*号代表所有班级;\n选择学生模式时，需要指定学生学号和学生名字')
        except Exception as e:
            print(str(e))
        finally:
            system("pause")


if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        print(str(e))
    # oldtime = datetime.now()
    # try:
    #     srcFilePath = "C:\\Users\\Lenovo\\Desktop\\党支部 未填报学生名单.xls"
    #     informUsers(srcFilePath)
    # except Exception as e:
    #     print(str(e))
    # finally:
    #     newtime = datetime.now()
    #     print("本次通知使用时间为%.2fs" % ((newtime - oldtime).total_seconds()))
